/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.eclipse.imagen.remote;

import java.awt.*;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.renderable.ParameterBlock;
import java.util.Vector;
import org.eclipse.imagen.*;
import org.eclipse.imagen.util.ImagingListener;

/**
 * A subclass of <code>PlanarImage</code> which represents an image on a remote server machine. This class is also an
 * implementation of the <code>RemoteRenderedImage</code> interface. This class allows processing to occur on the remote
 * server machine.
 *
 * <p>Conceptually this class is like a No-op, all it provides is a mechanism allowing the processing to occur on a
 * server. Note that this class does not mandate that the client-server communication rely on any particular wire
 * protocol or communication protocol. A subclass can choose any wire or communication protocol to communicate with its
 * server. This is accomplished by having the subclass implement the methods declared to be abstract in this class. All
 * functionality in this class is then implemented in terms of these abstract methods.
 *
 * <p>Network errors (detected via throws of <code>RemoteImagingException</code>) are dealt with through the use of
 * retry intervals and retries. Retries refers to the maximum number of times a remote operation will be retried. The
 * retry interval refers to the amount of time in milliseconds between two consecutive retries. If errors are
 * encountered at each retry and the number of specified retries has been exhausted, a <code>RemoteImagingException
 * </code> will be thrown. Time outs (When the amount of time taken to get a response or the result of an operation from
 * the remote machine exceeds a limit) are not dealt with, and must be taken care of by the network imaging protocol
 * implementation. The implementation must be responsible for monitoring time outs, but on encountering one can deal
 * with it by throwing a <code>RemoteImagingException</code>, which will then be dealt with using retries and retry
 * intervals.
 *
 * <p>The resultant image layout is computed and provided by the concrete subclass by implementing the abstract method
 * <code>getImageLayout</code>. All the accessor methods dealing with the layout variables namely <code>getMinX()</code>
 * , <code>getMinY()</code>, <code>getWidth()</code>, <code>getHeight()</code>, <code>getMaxX()</code>, <code>getMaxY()
 * </code>, <code>getTileWidth()</code>, <code>getTileHeight()</code>, <code>getTileGridXOffset()</code>, <code>
 * getTileGridYOffset()</code>, <code>getColorModel()</code> and <code>getSampleModel()</code> are implemented in terms
 * of the <code>getImageLayout()</code> method. The implementation of these methods uses retries and retry intervals to
 * deal with Network errors, such that the subclass implementing <code>getImageLayout()</code> does not need to worry
 * about Network errors except to signal them by throwing a <code>RemoteImagingException</code>. The same applies to the
 * other abstract methods implemented by sub-classes namely <code>getRemoteProperty()</code>, <code>
 * getRemotePropertyNames()</code> and <code>computeTile()</code>.
 *
 * <p>The <code>getTile</code> method (abstract in this class' superclass), is implemented in terms of the <code>
 * computeTile</code> method. It provides the additional functionality of caching the tiles on the client, as well as
 * that of dealing with Network errors as mentioned above.
 *
 * @see RemoteImagingException
 * @since JAI 1.1
 */
public abstract class PlanarImageServerProxy extends PlanarImage implements RemoteRenderedImage {

    /** Time in milliseconds between retries. */
    protected int retryInterval;

    /** The number of retries. */
    protected int numRetries;

    /** A reference to a centralized TileCache object. */
    protected transient TileCache cache;

    /**
     * Metric used to produce an ordered list of tiles. This determines which tiles are removed from the cache first if
     * a memory control operation is required.
     */
    protected Object tileCacheMetric;

    /** A reference to the OperationRegistry object. */
    protected transient OperationRegistry registry;

    /** The String representing the remote server machine. */
    protected String serverName;

    /** The name of the protocol to be used for remote communication. */
    protected String protocolName;

    /** The name of the operation to be performed remotely. */
    protected String operationName;

    /** The sources and/or arguments to the operation. */
    protected ParameterBlock paramBlock;

    /** The RenderingHints for the operation. */
    protected RenderingHints hints;

    // The layout of the image
    private ImageLayout layout = null;

    /** The preferences to be utilized in the negotiation. */
    protected NegotiableCapabilitySet preferences;

    /**
     * The set of properties agreed upon after the negotiation process between the client and the server has been
     * completed.
     */
    protected NegotiableCapabilitySet negotiated;

    /** The server capabilities. */
    NegotiableCapabilitySet serverCapabilities;

    /** The client capabilities. */
    NegotiableCapabilitySet clientCapabilities;

    /**
     * Checks that all the layout parameters have been specified.
     *
     * @throws IllegalArgumentException if layout is null.
     * @throws Error if all the layout fields are not initialized.
     */
    private static void checkLayout(ImageLayout layout) {

        if (layout == null) {
            throw new IllegalArgumentException("layout is null.");
        }

        if (layout.getValidMask() != 0x3ff) {
            throw new Error(JaiI18N.getString("PlanarImageServerProxy3"));
        }
    }

    /**
     * Constructs a <code>PlanarImageServerProxy</code> using the specified name of the server to perform the specified
     * operation on, using the sources and parameters specified by the supplied <code>ParameterBlock</code> and supplied
     * <code>RenderingHints</code>. If hints relating to the <code>OperationRegistry</code>, <code>TileCache</code>,
     * retry interval, number of retries, tile caching metric or negotiation preferences are included in the specified
     * <code>RenderingHints</code> object, they will be honored.
     *
     * <p>An <code>IllegalArgumentException</code> may be thrown by the protocol specific classes at a later point, if
     * null is provided as the serverName argument and null is not considered a valid server name by the specified
     * protocol.
     *
     * @param serverName The <code>String</code> identifying the remote server machine.
     * @param protocolName The name of the remote imaging protocol.
     * @param operationName The name of the operation.
     * @param paramBlock The source(s) and/or parameter(s) for the operation.
     * @param hints The hints for the operation.
     * @throws IllegalArgumentException if operationName is null.
     */
    public PlanarImageServerProxy(
            String serverName,
            String protocolName,
            String operationName,
            ParameterBlock paramBlock,
            RenderingHints hints) {

        // To initialize property and event management stuff, as done by
        // superclass PlanarImage constructor.
        super(null, null, null);

        if (operationName == null) {
            throw new IllegalArgumentException(JaiI18N.getString("PlanarImageServerProxy1"));
        }

        this.serverName = serverName;
        this.protocolName = protocolName;
        this.operationName = operationName;
        this.paramBlock = paramBlock;
        this.hints = hints;

        if (hints == null) {
            // If there are no hints specified, use default values
            registry = JAI.getDefaultInstance().getOperationRegistry();
            cache = JAI.getDefaultInstance().getTileCache();
            retryInterval = RemoteJAI.DEFAULT_RETRY_INTERVAL;
            numRetries = RemoteJAI.DEFAULT_NUM_RETRIES;

            // Do negotiation even though there are no preferences, so that
            // negotiation takes place between the client and the server.
            setNegotiationPreferences(null);
        } else {

            registry = (OperationRegistry) hints.get(JAI.KEY_OPERATION_REGISTRY);
            if (registry == null) {
                registry = JAI.getDefaultInstance().getOperationRegistry();
            }

            cache = (TileCache) hints.get(JAI.KEY_TILE_CACHE);
            if (cache == null) {
                cache = JAI.getDefaultInstance().getTileCache();
            }

            Integer integer = (Integer) hints.get(RemoteJAI.KEY_RETRY_INTERVAL);
            if (integer == null) {
                retryInterval = RemoteJAI.DEFAULT_RETRY_INTERVAL;
            } else {
                retryInterval = integer.intValue();
            }

            integer = (Integer) hints.get(RemoteJAI.KEY_NUM_RETRIES);
            if (integer == null) {
                numRetries = RemoteJAI.DEFAULT_NUM_RETRIES;
            } else {
                numRetries = integer.intValue();
            }

            tileCacheMetric = (Object) hints.get(JAI.KEY_TILE_CACHE_METRIC);

            // Cause negotiation to take place.
            setNegotiationPreferences((NegotiableCapabilitySet) hints.get(RemoteJAI.KEY_NEGOTIATION_PREFERENCES));
        }

        if (paramBlock != null) {
            setSources(paramBlock.getSources());
        }
    }

    /** Returns the <code>String</code> that identifies the server. */
    public String getServerName() {
        return serverName;
    }

    /** Returns the <code>String</code> that identifies the remote imaging protocol. */
    public String getProtocolName() {
        return protocolName;
    }

    /** Returns the operation name as a <code>String</code>. */
    public String getOperationName() {
        return operationName;
    }

    /**
     * Returns the <code>ParameterBlock</code> that specifies the sources and parameters for the operation to be
     * performed by this <code>PlanarImageServerProxy</code>.
     */
    public ParameterBlock getParameterBlock() {
        return paramBlock;
    }

    /**
     * Returns the <code>RenderingHints</code> associated with the operation to be performed by this <code>
     * PlanarImageServerProxy</code>.
     */
    public RenderingHints getRenderingHints() {
        return hints;
    }

    /**
     * Returns the tile cache object of this image by reference. If this image does not have a tile cache, this method
     * returns <code>null</code>.
     */
    public TileCache getTileCache() {
        return cache;
    }

    /**
     * Sets the tile cache object of this image. A <code>null</code> input indicates that this image should have no tile
     * cache and subsequently computed tiles will not be cached.
     *
     * <p>The existing cache object is informed to release all the currently cached tiles of this image.
     *
     * @param cache A cache object to be used for caching this image's tiles, or <code>null</code> if no tile caching is
     *     desired.
     */
    public void setTileCache(TileCache cache) {
        if (this.cache != null) {
            this.cache.removeTiles(this);
        }
        this.cache = cache;
    }

    /** Returns the <code>tileCacheMetric</code> instance variable by reference. */
    public Object getTileCacheMetric() {
        return tileCacheMetric;
    }

    /**
     * Get the layout of the image. This could be implemented by either asking the server to specify the layout, or have
     * the client compute the image layout. The <code>ImageLayout</code> object returned must have all its fields
     * initialized, else an <code>Error</code> will be thrown. Network errors encountered should be signalled by
     * throwing a <code>RemoteImagingException</code>.
     *
     * @throws RemoteImagingException if an error condition during remote image processing occurs
     * @throws Error if all the fields in the ImageLayout are not initialized.
     */
    public abstract ImageLayout getImageLayout() throws RemoteImagingException;

    /**
     * Returns a property from the property set generated on the remote server machine. Network errors encountered
     * should be signalled by throwing a <code>RemoteImagingException</code>. If the property name is not recognized,
     * java.awt.Image.UndefinedProperty will be returned.
     *
     * @throws RemoteImagingException if an error condition during remote image processing occurs
     * @throws IllegalArgumentException if name is null.
     */
    public abstract Object getRemoteProperty(String name) throws RemoteImagingException;

    /**
     * Returns a list of names recognized by the <code>getRemoteProperty</code> method. Network errors encountered
     * should be signalled by throwing a <code>RemoteImagingException</code>.
     *
     * @throws RemoteImagingException if an error condition during remote image processing occurs
     */
    public abstract String[] getRemotePropertyNames() throws RemoteImagingException;

    /**
     * Returns a conservative estimate of the destination region that can potentially be affected by the pixels of a
     * rectangle of a given source. This can be implemented by either asking the server to compute the destination
     * region, or by having the client compute the destination region. Network errors encountered should be signalled by
     * throwing a <code>RemoteImagingException</code>.
     *
     * @param sourceRect The <code>Rectangle</code> in source coordinates.
     * @param sourceIndex The index of the source image.
     * @return A <code>Rectangle</code> indicating the potentially affected destination region, or <code>null</code> if
     *     the region is unknown.
     * @throws IllegalArgumentException If the source index is negative or greater than that of the last source.
     * @throws IllegalArgumentException If <code>sourceRect</code> is <code>null</code>.
     */
    public abstract Rectangle mapSourceRect(Rectangle sourceRect, int sourceIndex) throws RemoteImagingException;

    /**
     * Returns a conservative estimate of the region of a specified source that is required in order to compute the
     * pixels of a given destination rectangle. Either the server or the client can compute the source region to
     * implement this method. Network errors encountered should be signalled by throwing a <code>RemoteImagingException
     * </code>.
     *
     * @param destRect The <code>Rectangle</code> in destination coordinates.
     * @param sourceIndex The index of the source image.
     * @return A <code>Rectangle</code> indicating the required source region.
     * @throws IllegalArgumentException If the source index is negative or greater than that of the last source.
     * @throws IllegalArgumentException If <code>destRect</code> is <code>null</code>.
     */
    public abstract Rectangle mapDestRect(Rectangle destRect, int sourceIndex) throws RemoteImagingException;

    /**
     * Returns tile (tileX, tileY) as computed on the remote server machine. Note that tileX and tileY are indices into
     * the tile array, not pixel locations. The <code>Raster</code> that is returned is a copy. Network errors
     * encountered should be signalled by throwing a <code>RemoteImagingException</code>.
     *
     * <p>Subclasses must implement this method to return a non-<code>null</code> value for all tile indices between
     * <code>getMinTile{X,Y}</code> and <code>getMaxTile{X,Y}</code>, inclusive. Tile indices outside of this region
     * should result in a return value of <code>null</code>.
     *
     * @param tileX the X index of the requested tile in the tile array.
     * @param tileY the Y index of the requested tile in the tile array.
     * @throws RemoteImagingException if an error condition during remote image processing occurs
     */
    public abstract Raster computeTile(int tileX, int tileY) throws RemoteImagingException;

    /** Returns the amount of time between retries in milliseconds. */
    public int getRetryInterval() {
        return retryInterval;
    }

    /**
     * Sets the amount of time between retries in milliseconds.
     *
     * @param retryInterval The amount of time (in milliseconds) to wait between retries.
     * @throws IllegalArgumentException if retryInterval is negative.
     */
    public void setRetryInterval(int retryInterval) {
        if (retryInterval < 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic3"));
        }
        this.retryInterval = retryInterval;
    }

    /** Returns the number of retries. */
    public int getNumRetries() {
        return numRetries;
    }

    /**
     * Sets the number of retries.
     *
     * @param numRetries The number of times an operation should be retried in case of a network error.
     * @throws IllegalArgumentException if numRetries is negative.
     */
    public void setNumRetries(int numRetries) {
        if (numRetries < 0) {
            throw new IllegalArgumentException(JaiI18N.getString("Generic4"));
        }
        this.numRetries = numRetries;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the X coordinate of the leftmost column of the remote
     * image.
     */
    public int getMinX() {
        requestLayout();
        return minX;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the X coordinate of the column immediately to the
     * right of the rightmost column of the remote image.
     */
    public int getMaxX() {
        requestLayout();
        return minX + width;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the Y coordinate of the uppermost row of the remote
     * image.
     */
    public int getMinY() {
        requestLayout();
        return minY;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the Y coordinate of the row immediately below the
     * bottom row of the remote image.
     */
    public int getMaxY() {
        requestLayout();
        return minY + height;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the width of the remote image. */
    public int getWidth() {
        requestLayout();
        return width;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the height of the remote image. */
    public int getHeight() {
        requestLayout();
        return height;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the width of a tile remotely. */
    public int getTileWidth() {
        requestLayout();
        return tileWidth;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the height of a tile remotely. */
    public int getTileHeight() {
        requestLayout();
        return tileHeight;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the X coordinate of the upper-left pixel of tile (0,
     * 0) remotely.
     */
    public int getTileGridXOffset() {
        requestLayout();
        return tileGridXOffset;
    }

    /**
     * Overrides the method in <code>PlanarImage</code> to return the Y coordinate of the upper-left pixel of tile (0,
     * 0) remotely.
     */
    public int getTileGridYOffset() {
        requestLayout();
        return tileGridYOffset;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the <code>SampleModel</code> of the remote image. */
    public SampleModel getSampleModel() {
        requestLayout();
        return sampleModel;
    }

    /** Overrides the method in <code>PlanarImage</code> to return the <code>ColorModel</code> of the remote image. */
    public ColorModel getColorModel() {
        requestLayout();
        return colorModel;
    }

    /**
     * Cause the subclass' <code>getImageLayout</code> method to be called, caching the <code>ImageLayout</code> object
     * locally.
     *
     * @throws IllegalArgumentException if getImageLayout returns null.
     * @throws Error if all the fields of the layout are not initialized.
     * @throws RemoteImagingException if the limit of retries is exceeded.
     */
    private ImageLayout requestLayout() {

        if (layout != null) return layout;

        Exception rieSave = null;
        int count = 0;
        while (count++ < numRetries) {
            try {
                layout = getImageLayout();
                // Check that all the fields are initialized
                checkLayout(layout);

                // Set all the super class variables
                minX = layout.getMinX(null);
                minY = layout.getMinY(null);
                width = layout.getWidth(null);
                height = layout.getHeight(null);
                tileWidth = layout.getTileWidth(null);
                tileHeight = layout.getTileHeight(null);
                tileGridXOffset = layout.getTileGridXOffset(null);
                tileGridYOffset = layout.getTileGridYOffset(null);
                sampleModel = layout.getSampleModel(null);
                colorModel = layout.getColorModel(null);
                break;
            } catch (RemoteImagingException e) {
                System.err.println(JaiI18N.getString("PlanarImageServerProxy0"));
                rieSave = e;
                // Sleep for retryInterval milliseconds before retrying.
                try {
                    Thread.sleep(retryInterval);
                } catch (InterruptedException f) {
                }
            }
        }

        if (layout == null) {
            sendExceptionToListener(rieSave);
        }

        return layout;
    }

    /**
     * Gets a property from the property set of this image. If the property name is not recognized, <code>
     * java.awt.Image.UndefinedProperty</code> will be returned. The property to be returned is first looked for in the
     * set of locally cached properties. If not found, the <code>getRemoteProperty</code> method is called to retrieve
     * the property. Network errors that might be encountered during the <code>getRemoteProperty</code> call are dealt
     * with by retries and retry intervals.
     *
     * @param name the name of the property to get, as a <code>String</code>.
     * @return a reference to the property <code>Object</code>, or the value <code>java.awt.Image.UndefinedProperty
     *     </code>.
     * @throws RemoteImagingException if the limit of retries is exceeded.
     */
    public Object getProperty(String name) {

        // Try to get property locally.
        Object property = super.getProperty(name);

        if (property == null || property == Image.UndefinedProperty) {

            Exception rieSave = null;
            int count = 0;
            while (count++ < numRetries) {
                try {
                    property = getRemoteProperty(name);
                    if (property != Image.UndefinedProperty) {
                        setProperty(name, property); // Cache property locally
                    }
                    return property;
                } catch (RemoteImagingException rie) {
                    System.err.println(JaiI18N.getString("PlanarImageServerProxy0"));
                    rieSave = rie;
                    try {
                        Thread.sleep(retryInterval);
                    } catch (InterruptedException ie) {
                    }
                }
            }

            sendExceptionToListener(rieSave);
            return property;
        } else {
            return property;
        }
    }

    /**
     * Returns a list of property names that are recognized by this image or <code>null</code> if none are recognized.
     * The list of recognized property names consists of the locally cached property names (retrieved via <code>
     * super.getPropertyNames</code>) as well as those that might be generated by the operations performed on the remote
     * server machine (retrieved via <code>getRemotePropertyNames</code>). Network errors that might be encountered
     * during the <code>getRemotePropertyNames</code> method are dealt with by retries and retry intervals.
     *
     * @return an array of <code>String</code>s containing valid property names.
     * @throws RemoteImagingException if the limit of retries is exceeded.
     */
    public String[] getPropertyNames() {

        // Retrieve local property names.
        String[] localPropertyNames = super.getPropertyNames();

        Vector names = new Vector();
        // Put local names in a Vector, if any
        if (localPropertyNames != null) {
            for (int i = 0; i < localPropertyNames.length; i++) {
                names.add(localPropertyNames[i]);
            }
        }

        int count = 0;
        String[] remotePropertyNames = null;
        Exception rieSave = null;

        while (count++ < numRetries) {
            try {
                remotePropertyNames = getRemotePropertyNames();
                break;
            } catch (RemoteImagingException rie) {
                System.err.println(JaiI18N.getString("PlanarImageServerProxy0"));
                rieSave = rie;
                try {
                    Thread.sleep(retryInterval);
                } catch (InterruptedException ie) {
                }
            }
        }

        if (count > numRetries) {
            sendExceptionToListener(rieSave);
        }

        // Put the remote names, if any, in the Vector.
        if (remotePropertyNames != null) {
            for (int i = 0; i < remotePropertyNames.length; i++) {
                if (!names.contains(remotePropertyNames[i])) {
                    names.add(remotePropertyNames[i]);
                }
            }
        }

        // Set the return value from the vector.
        String propertyNames[] = names.size() == 0 ? null : (String[]) names.toArray(new String[names.size()]);

        return propertyNames;
    }

    /**
     * Returns the tile (tileX, tileY). Note the tileX and tileY are indices into the tile array and not pixel
     * positions. This method is implemented in terms of the <code>computeTile</code> method. This method deals with
     * Network errors (recognized as <code>RemoteImagingExceptions</code>) through retries and retry intervals. This
     * method also performs caching of tiles, so that an already computed tile does not need to be re-computed.
     *
     * @param tileX the X index of the tile.
     * @param tileY the Y index of the tile.
     * @throws RemoteImagingException if limit of retries is exceeded.
     */
    public Raster getTile(int tileX, int tileY) {

        Raster tile = null;

        // Make sure the requested tile is inside this image's boundary.
        if (tileX >= getMinTileX() && tileX <= getMaxTileX() && tileY >= getMinTileY() && tileY <= getMaxTileY()) {

            // Check if tile is available in the cache.
            tile = cache != null ? cache.getTile(this, tileX, tileY) : null;

            if (tile == null) { // tile not in cache
                // Ask the subclass for the tile
                int count = 0;
                Exception rieSave = null;
                while (count++ < numRetries) {
                    try {
                        tile = computeTile(tileX, tileY);
                        break;
                    } catch (RemoteImagingException rie) {
                        System.err.println(JaiI18N.getString("PlanarImageServerProxy0"));
                        rieSave = rie;
                        try {
                            Thread.sleep(retryInterval);
                        } catch (InterruptedException ie) {

                        }
                    }
                }

                if (count > numRetries) {
                    sendExceptionToListener(rieSave);
                }

                // Cache the result tile.
                if (cache != null) {
                    cache.add(this, tileX, tileY, tile, tileCacheMetric);
                }
            }
        }

        return tile;
    }

    /** Uncaches all the tiles when this image is garbage collected. */
    @Override
    public void dispose() {
        if (cache != null) {
            cache.removeTiles(this);
        }
        super.dispose();
    }

    //
    // NEGOTIATION RELATED METHODS
    //

    /** Returns the current negotiation preferences or null, if none were set previously. */
    public NegotiableCapabilitySet getNegotiationPreferences() {
        return preferences;
    }

    /**
     * Sets the preferences to be used in the client-server communication. These preferences are utilized in the
     * negotiation process. Note that preferences for more than one category can be specified using this method. Also
     * each preference can be a list of values in decreasing order of preference, each value specified as a <code>
     * NegotiableCapability</code>. The <code>NegotiableCapability</code> first (for a particular category) in this list
     * is given highest priority in the negotiation process (for that category).
     *
     * <p>It may be noted that this method allows for multiple negotiation cycles. Everytime this method is called, new
     * preferences are specified for the negotiation, which takes place anew to produce a new set of negotiated
     * resultant values to be used in the remote communication. If the subclass wants to ignore the negotiation
     * preferences newly set, this method can be overridden to do so.
     *
     * @param preferences The preferences to be used in the negotiation process.
     */
    public void setNegotiationPreferences(NegotiableCapabilitySet preferences) {
        this.preferences = preferences;

        // Every time new preferences are set, invalidate old Negotiation
        // results and do the negotiation again.
        negotiated = null;

        getNegotiatedValues();
    }

    /**
     * Returns the results of the negotiation process. This will return null if no negotiation preferences were set, and
     * no negotiation was performed, or if the negotiation failed.
     */
    public synchronized NegotiableCapabilitySet getNegotiatedValues() throws RemoteImagingException {

        // If negotiation was not performed before, or if new preferences
        // have invalidated the old negotiated results.
        if (negotiated == null) {

            // Initialize the clientCapabilities and serverCapabilities
            // variables
            getCapabilities();

            // Do the negotiation
            negotiated = RemoteJAI.negotiate(preferences, serverCapabilities, clientCapabilities);

            // Inform the server of the negotiated values.
            setServerNegotiatedValues(negotiated);
        }

        return negotiated;
    }

    /**
     * Returns the results of the negotiation process for the given category. This will return null if no negotiation
     * preferences were set, and no negotiation was performed, or if the negotiation failed.
     *
     * @param category The category to return the negotiated results for.
     * @throws IllegalArgumentException if category is null.
     */
    public NegotiableCapability getNegotiatedValue(String category) throws RemoteImagingException {

        // We do not need to check for category being null, since that
        // check will be made by the methods called from within this method.

        // If negotiation was not performed before, or if new preferences
        // have invalidated the old negotiated results.
        if (negotiated == null) {

            getCapabilities();

            // Do the negotiation
            return RemoteJAI.negotiate(preferences, serverCapabilities, clientCapabilities, category);
        } else {
            // If negotiated is not null, then the negotiated results are
            // current and the result for the given category can just be
            // extracted from there and returned.
            return negotiated.getNegotiatedValue(category);
        }
    }

    // Get the client and server capabilities
    private void getCapabilities() {

        String mode = "remoteRendered";
        if (serverCapabilities == null) {

            RemoteDescriptor desc = (RemoteDescriptor) registry.getDescriptor(mode, protocolName);

            int count = 0;
            Exception rieSave = null;
            while (count++ < numRetries) {
                try {
                    serverCapabilities = desc.getServerCapabilities(serverName);
                    break;
                } catch (RemoteImagingException rie) {
                    System.err.println(JaiI18N.getString("PlanarImageServerProxy0"));
                    rieSave = rie;
                    try {
                        Thread.sleep(retryInterval);
                    } catch (InterruptedException ie) {
                    }
                }
            }

            if (count > numRetries) {
                sendExceptionToListener(rieSave);
            }
        }

        if (clientCapabilities == null) {
            RemoteRIF rrif = (RemoteRIF) registry.getFactory(mode, protocolName);

            clientCapabilities = rrif.getClientCapabilities();
        }
    }

    void sendExceptionToListener(Exception e) {
        ImagingListener listener = null;
        if (hints != null) listener = (ImagingListener) hints.get(JAI.KEY_IMAGING_LISTENER);
        else listener = JAI.getDefaultInstance().getImagingListener();
        String message = JaiI18N.getString("PlanarImageServerProxy2");
        listener.errorOccurred(message, new RemoteImagingException(message, e), this, false);
    }
}
